//===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
/// Utility methods for parsing values into [JsonSchema].
@ModuleInfo { minPklVersion = "0.25.0" }
module org.json_schema.Parser

import "pkl:json"
import "pkl:reflect"

import "JsonSchema.pkl"

local jsonParser = new json.Parser { useMapping = true }

local knownSchemaKeys: Set<String> = reflect.Module(JsonSchema).moduleClass.properties.keys

local function toJsonSchema(
  raw: Mapping|Boolean,
  baseSchema: JsonSchema?,
  path: List<String|Int>
): JsonSchema.Schema =
  if (raw is Boolean)
    raw
  else
    new {
      $$baseSchema = baseSchema ?? this
      $id = raw.getOrNull("$id")
      $ref = raw.getOrNull("$ref")
      $schema = raw.getOrNull("$schema")
      $comment = raw.getOrNull("$comment")
      $defs = (raw.getOrNull("$defs") as Mapping?)
        ?.toMap()
        ?.map((key, value) -> Pair(key, toJsonSchema(value, $$baseSchema, path.add("definitions").add(key))))
        ?.toMapping()
      definitions = (raw.getOrNull("definitions") as Mapping?)
        ?.toMap()
        ?.map((key, value) -> Pair(key, toJsonSchema(value, $$baseSchema, path.add("$defs").add(key))))
        ?.toMapping()
      type = raw.getOrNull("type")
      format = raw.getOrNull("format")
      pattern = raw.getOrNull("pattern")
      maxLength = raw.getOrNull("maxLength")
      minLength = raw.getOrNull("minLength")
      title = raw.getOrNull("title")
      description = raw.getOrNull("description")
      multipleOf = raw.getOrNull("multipleOf")
      minimum = raw.getOrNull("minimum")
      maximum = raw.getOrNull("maximum")
      exclusiveMinimum = raw.getOrNull("exclusiveMinimum")
      exclusiveMaximum = raw.getOrNull("exclusiveMaximum")
      examples = raw.getOrNull("examples")
      allOf = (raw.getOrNull("allOf") as Listing?)
        ?.toList()
        ?.mapIndexed((idx, elem) -> toJsonSchema(elem, $$baseSchema, path.add("allOf").add(idx)))
        ?.toListing()
      anyOf = (raw.getOrNull("anyOf") as Listing?)
        ?.toList()
        ?.mapIndexed((idx, elem) -> toJsonSchema(elem, $$baseSchema, path.add("anyOf").add(idx)))
        ?.toListing()
      oneOf = (raw.getOrNull("oneOf") as Listing?)
        ?.toList()
        ?.mapIndexed((idx, elem) -> toJsonSchema(elem, $$baseSchema, path.add("oneOf").add(idx)))
        ?.toListing()
      not = if (raw.containsKey("not")) toJsonSchema(raw["not"], $$baseSchema, path.add("not")) else null
      properties = (raw.getOrNull("properties") as Mapping?)
        ?.toMap()
        ?.map((key, value) -> Pair(key, toJsonSchema(value, $$baseSchema, path.add("properties").add(key))))
        ?.toMapping()
      items =
        let (rawItems = raw.getOrNull("items"))
          if (rawItems == null) null
          else if (rawItems is Listing)
            rawItems
              .toList()
              .mapIndexed((idx, elem) -> toJsonSchema(elem, $$baseSchema, path.add("items").add(idx)))
              .toListing()
          else toJsonSchema(rawItems, $$baseSchema, path.add("items"))
      additionalItems =
        let (rawAdditionalItems = raw.getOrNull("additionalItems"))
          if (rawAdditionalItems == null) null
          else toJsonSchema(rawAdditionalItems, $$baseSchema, path.add("additionalItems"))
      contains =
        let (rawContains = raw.getOrNull("contains"))
          if (rawContains == null) null
          else toJsonSchema(rawContains, $$baseSchema, path.add("contains"))
      minItems = raw.getOrNull("minItems")
      maxItems = raw.getOrNull("maxItems")
      uniqueItems = raw.getOrNull("uniqueItems")
      default = raw.getOrNull("default")
      deprecated = raw.getOrNull("deprecated")
      readOnly = raw.getOrNull("readOnly")
      writeOnly = raw.getOrNull("writeOnly")
      enum = raw.getOrNull("enum")
      required = raw.getOrNull("required")
      `const` = raw.getOrNull("const")
      propertyNames = let (_raw = raw.getOrNull("propertyNames"))
          if (_raw == null) null
          else toJsonSchema(_raw, $$baseSchema, path.add("propertyNames"))
      maxProperties = raw.getOrNull("maxProperties")
      minProperties = raw.getOrNull("minProperties")
      additionalProperties =
        let (rawAdditionalProperties = raw.getOrNull("additionalProperties"))
          if (rawAdditionalProperties == null) null
          else toJsonSchema(rawAdditionalProperties, $$baseSchema, path.add("additionalProperties"))
      patternProperties = (raw.getOrNull("patternProperties") as Mapping?)
        ?.toMap()
        ?.map((key, value) -> Pair(key, toJsonSchema(value, $$baseSchema, path.add("patternProperties").add(key))))
        ?.toMapping()
      _inline_ = raw.toMap()
        .filter((key, value) -> !knownSchemaKeys.contains(key) && value is Boolean|Mapping)
        .map((key, value) -> Pair(key, toJsonSchema(value, $$baseSchema, path.add(key))))
        .toMapping()
    }

/// Given a JSON string or [Resource], parse it into a [JsonSchema.Schema] instance.
function parse(src: Resource|String): JsonSchema.Schema =
  let (jsonParsed: Mapping|Boolean = jsonParser.parse(src) as Mapping|Boolean)
    toJsonSchema(jsonParsed, null, List())
